Effective C++ 条款44 防止代码膨胀
条款44 : 将与参数无关的代码抽离templates
我们知道代码重复和过度的inline都可能导致代码膨胀, 而在模板中会发生比较隐晦的代码重复, 我们应当尽力去避免代码重复的情况发生, 而最核心的方法就是将与参数无关的代码抽离templates, 让我们通过本条款进行了解.
在本条款中我们将会了解 :
- 在模板中什么情况下会发生代码膨胀?
- 如何缓解这种代码膨胀?
- 在缓解代码膨胀后如何知道该操作什么数据?
代码膨胀
举个例子, 假设你想为固定尺寸的正方矩形编写一个template, 该矩形指出逆矩阵运算 :
1 |
|
当我们在使用这个类型时 :
1 |
|
我们应该已经注意到, 只要T和n有不同, 在实际上编译器就会重新编译出一份代码. 在本例中, 仅仅只是在尺寸上有差别, 但还是在底层编译出了两份除了尺寸不一样但是其他都相近的代码, 这就造成了实际意义上的代码膨胀.
你也许会觉得这样无可厚非, 这就是模板机制导致的, 但是细想invert()
这个函数, 对矩阵求逆的过程基本都是相同的, 只是矩阵的尺寸有差别罢了, 我们可以通过某些手法将invert()
抽离出来, 使其不必频繁编译, 这便是我们接下来要介绍的解决方法.
将实现类和功能类分离
我们可以把一个类拆分, 把功能函数拆分出来作为基类, 实现类作为子类.
让我们直接给出例子, 根据例子来介绍 :
1 |
|
这里SquareMatrix
是实现类, 也就是我们正常使用的类型, 它的情况还是和上面一致, 只要T和n有不同, 还是会生成一份额外的代码, 但是不同的是它把大量的实际代码移到了功能类中(这里只显示了invert, 实际可以有很多功能函数), 使本身的代码量骤减, 大大减少了额外代码的生成.
这里SquareMatrixBase
是功能类, 他负责给派生类提供相应的功能, 我们从派生类private继承自它便可看出. 它只有一个T模板参数, 也就是说它只对”矩阵元素对象的类型”参数化, 不对”矩阵的尺寸”参数化, 也就是说只要T相同, 就算派生类的n是任何数字, 都将使用同一份代码 , 不会再编译出多份代码, 我们也可以认为实际上是用函数参数替代掉了SquareMatrixBase的模板参数.
数据操作问题
在解释这个问题之前, 我们应该再引入一个前提, 就是SquareMatrix应该是有一个存储数据的成员变量的, 这应当很容易理解, 在最初版SquareMatrix
中就可以是这样 :
1 |
|
在最初版中, invert()
可以直接对data进行操作, 但是当我们把实现类和功能类分离后, data肯定还是在实现类中, 因为控制数据应当是实现类的职责, 并且这样子便于动态内存的分配; 但是invert()到了功能类, 无法直接对data进行修改, 功能类函数该如何实际修改data中的数据, 这便是我们要解决的问题.
其实解决方式也很简单, 在SquareMatrixBase中存储一个指向data的指针就好了 :
1 |
|
我们可以自行决定实现类中数据内存的分配方式, 在上文中T data[n*n];
是将数据存储在了对象内部, 也就是栈上. 我们也可以通过动态分配内存的方式将数据存入堆上(通过new来分配内存) :
1 |
|
类型参数导致的代码膨胀
我们可以发现上文都对非类型参数(n)导致的代码膨胀提供的解决方案, 但是类型参数(T)也同样会导致代码膨胀, 不同的T也会产生不同的编译版本, 有些类型在底层其实是非常相近甚至相同的, 例如int和long, 各种指针类型之间.
假设T是一个指针类型, 所有指针类型都有着相同的二进制表述, 其实编译出来的代码基本一致, 只是指针类型不一样而已. 那么我们就可以在模板函数中将这些指针转换为void*
, 然后调用操作void*
指针类型的函数, 由后者完成实际函数, 也可以达到类似防止代码膨胀的效果, 标准库中的vector, list等都用过这种方式, 以下是list在底层的类似实现 :
1 |
|
请记住 :
使用模板会有隐含的代码膨胀产生, 我们可以通过将一些功能函数抽离出来作为基类, private继承给派生类来避免代码膨胀.
因非类型模板参数(n)造成的代码膨胀, 往往可消除, 可以用函数参数或class成员变量替换掉非类型模板参数.
因类型模板参数(T)造成的代码膨胀, 往往可降低, 可以让底层二进制表述完全相同的类型(如指针)共享功能函数.
by 天目中云